/*
 *  NFCSigning - Open source library for signing/validation of NDEF messages
 *  Copyright (C) 2009-2010 The NFCSigning Team
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; either
 *  version 2.1 of the License, or (at your option) any later version.
 *
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 *
 */
package org.nfcsigning;

import org.nfcsigning.algorithm.DefaultVerifierFactory;
import org.nfcsigning.algorithm.PublicKey;
import org.nfcsigning.algorithm.SignatureVerifier;
import org.nfcsigning.algorithm.VerifierFactory;
import org.nfcsigning.certificate.Certificate;
import org.nfcsigning.certificate.CertificateChain;
import org.nfcsigning.certificate.CertificateChainFactory;
import org.nfcsigning.certificate.DefaultCertificateChainFactory;
import org.nfcsigning.certificate.InvalidCertificateChain;
import org.nfcsigning.certificate.UnsupportedAlgorithmException;
import org.nfcsigning.record.CertificateBytes;
import org.nfcsigning.record.InvalidSignatureRecord;
import org.nfcsigning.record.SignatureRecord;
import org.nfcsigning.record.URIRecord;
import java.io.IOException;
import java.util.Hashtable;

//#ifdef MyJSR257
//# import my.javax.microedition.contactless.ndef.NDEFMessage;
//# import my.javax.microedition.contactless.ndef.NDEFRecord;
//# import my.javax.microedition.contactless.ndef.NDEFRecordType;
//# import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
//#else
import javax.microedition.contactless.ndef.NDEFMessage;
import javax.microedition.contactless.ndef.NDEFRecord;
import javax.microedition.contactless.ndef.NDEFRecordType;
//#endif

/**
 *
 * @author Markus Kilås
 */
public class MessageValidator {

    private Certificate[] trustedCertificates;
    private SignatureFetcher signatureFetcher;
    private CertificateFetcher certificateFetcher;
    private VerifierFactory verifierFactory;
    private CertificateChainFactory certificateChainFactory;

    public MessageValidator(Certificate[] trustedCerts) {
        this(trustedCerts, null, null);
    }

    public MessageValidator(Certificate[] trustedCerts, SignatureFetcher signatureFetcher, CertificateFetcher certFetcher) {
        this.trustedCertificates = trustedCerts;
        this.signatureFetcher = signatureFetcher;
        this.certificateFetcher = certFetcher;
    }

    public CertificateFetcher getCertificateFetcher() {
        return certificateFetcher;
    }

    public SignatureFetcher getSignatureFetcher() {
        return signatureFetcher;
    }

    public Certificate[] getTrustedCertificates() {
        return trustedCertificates;
    }

    public VerifierFactory getVerifierFactory() {
        return verifierFactory;
    }

    public void setVerifierFactory(VerifierFactory verifierFactory) {
        this.verifierFactory = verifierFactory;
    }

    public CertificateChainFactory getCertificateChainFactory() {
        return certificateChainFactory;
    }

    public void setCertificateChainFactory(CertificateChainFactory certificateChainFactory) {
        this.certificateChainFactory = certificateChainFactory;
    }

    public SignedMessage validate(NDEFMessage message) {        // TODO: Implement this a better way
        int[] indexes = new int[message.getNumberOfRecords()];
        for(int i = 0; i < indexes.length; i++) {
            indexes[i] = i;
        }
        return validate(message, indexes);
    }

    public SignedMessage validate(NDEFMessage message, int[] recordIndexes) {
        return validate(message, recordIndexes, this);
    }
    
    SignedMessage validate(final NDEFMessage msg, int[] indexes, MessageValidator validator) {
        
        Signature[] recordSignatures = new Signature[msg.getNumberOfRecords()];
        
        // Map each record index to a signed range
        Hashtable signatureMap = splitIntoRangesMap(msg.getRecords());
        
        for(int i = 0; i < indexes.length; i++) {
            int j = indexes[i];
            
            SignedRange range = (SignedRange) signatureMap.get(new Integer(j));
            SignatureStatus status;
            
            if(range == null) {
                status = SignatureStatus.invalid("No signature");
//                recordSignatures[j] = new Signature(null, status);
            } else {
                
                status = range.getStatus();
                
                // If this range is not already validated
                if(status == null) {
                
                    // Validate this signed range
                    status = validateSignedRange(range, msg.getRecords(), validator);
                    range.setStatus(status);
                }
                recordSignatures[j] = new Signature(range.getSignatureRecord(), range.getStatus());
            }
            
//            recordSignatures[j] = new Signature(range.getSignatureRecord(), range.getStatus());
        }
        
        return new SignedMessage(recordSignatures) {

            public NDEFMessage toNDEFMessage() {
                return msg;
            }
            
        };
    }

    protected boolean isSignatureRecord(NDEFRecordType recordType) {
        return SignatureRecord.RECORD_TYPE.equals(recordType);
    }
    
    Hashtable splitIntoRangesMap(NDEFRecord[] rs) {
        
        SignedRange[] ranges = new SignedRange[rs.length];
        
        SignedRange currentRange = new SignedRange();
        
        for(int i = 0; i < rs.length; i++) {
            
            // Signature Record or Marker
            if(isSignatureRecord(rs[i].getRecordType())) {
                            
                try {
                    SignatureRecord signRec = new SignatureRecord(rs[i]);
                    
                    if(!signRec.isMarker()) {
                        // Save this record in current range
                        currentRange.setSignatureRecord(signRec);
                    }

                    currentRange.setSignatureRecordType(rs[i].getRecordType());
                    
                } catch (InvalidSignatureRecord ex) {
                    ex.printStackTrace(); // TODO: Should we abort everything or try to recover or leave it as this
                }

                // Start a new range
                currentRange = new SignedRange();
                continue;
            }
            
            currentRange.add(new Integer(i));
            //map.put(new Integer(i), currentRange);
            ranges[i] = currentRange;
        }
        
        // Result
        Hashtable map = new Hashtable();
        for(int i = 0; i < ranges.length; i++) {
            if(ranges[i] != null && ranges[i].getSignatureRecord() != null) {
                map.put(new Integer(i), ranges[i]);
            }
        }
        
        return map;
    }

    static SignatureStatus validateSignedRange(SignedRange range, NDEFRecord[] records, MessageValidator validator) {
        
        SignatureFetcher signFetcher = validator.getSignatureFetcher();
        Certificate[] trustedCerts = validator.getTrustedCertificates();
        CertificateFetcher certFetcher = validator.getCertificateFetcher();
        
        VerifierFactory verifierFactory = validator.getVerifierFactory();
        if(verifierFactory == null) {
            verifierFactory = new DefaultVerifierFactory();
        }
        
        CertificateChainFactory certChainFactory = validator.getCertificateChainFactory();
        if(certChainFactory == null) {
            certChainFactory = new DefaultCertificateChainFactory();
        }
        
        SignatureRecord sigRec = range.getSignatureRecord();
        
        CertificateChain chain;
        CertificateBytes[] chainBytes;
        
        try {
            
            chainBytes = sigRec.getCertificateField().getChain();
            
            chain = certChainFactory.createInstance(range.getSignatureRecordType(), sigRec.getCertificateField().getFormat(), chainBytes);
            if(chain == null) {
                return SignatureStatus.invalid("Unsupported certificate type: " + sigRec.getCertificateField().getFormat());
            }
            
            URIRecord chainURI = sigRec.getCertificateField().getUri();
            if(chainURI != null) {
                CertificateChain chainFromUri = certFetcher.fetchChain(chainURI);
                if(chainFromUri != null) {
                    chain = chain.concat(chainFromUri);
                }
            }
            
            if(!chain.validateChain(trustedCerts, verifierFactory)) {
                return SignatureStatus.invalid("Invalid certificate chain: " + chain.getValidationMessage());    
            }
            
            // Verify signature
            byte[] coveredBytes = Canonicalizer.toCanonicalizedForm(records, range.getRecordIndexes());

            return verifySignature(coveredBytes, sigRec, range.getSignatureRecordType(), signFetcher, chain, verifierFactory);
            
        } catch (FetcherException ex) {
            ex.printStackTrace();
            return SignatureStatus.invalid("Certificate could not be fetched: " + ex.getMessage());
        } catch (UnsupportedAlgorithmException ex) {
            ex.printStackTrace();
            return SignatureStatus.invalid("Unsupported algorithm in chain: " + ex.getMessage());
        } catch (InvalidCertificateChain ex) {
            ex.printStackTrace();
            return SignatureStatus.invalid("Invalid certificate chain: " + ex.getMessage());
        } catch(IOException ex) {
            ex.printStackTrace();
            return SignatureStatus.invalid("Error: " + ex.getMessage());
        }
    }
    
    static SignatureStatus verifySignature(byte[] coveredBytes, SignatureRecord sigRec, NDEFRecordType signatureRecordType, SignatureFetcher signFetcher, CertificateChain chain, VerifierFactory verifierFactory) {
        
        SignatureVerifier verifier = verifierFactory.getVerifier(signatureRecordType, sigRec.getSignatureField().getType());
        if(verifier == null) {
            return SignatureStatus.invalid("Signature type not supported");
        }
        
        byte[] signatureBytes;
        URIRecord signUri = sigRec.getSignatureField().getUri();
        
        boolean consistent;
        try {
        
            if(signUri == null) {
                signatureBytes = sigRec.getSignatureField().getSignature();
            } else {
                signatureBytes = signFetcher.fetchSignature(signUri);
            }

            PublicKey pubKey = chain.getPublicKey(chain.getUserCertificate());

//            SubjectPublicKeyInfo subjectPublicKeyInfo = chain.getUserCertificate().getSubjectPublicKeyInfo();


            consistent = verifier.verifySignature(signatureBytes, coveredBytes, pubKey);
        } catch (FetcherException ex) {
            ex.printStackTrace();
            return SignatureStatus.invalid("Error: " + ex.getMessage());
        } catch (org.nfcsigning.algorithm.SignatureException ex) {
            return SignatureStatus.invalid("Error: " + ex.getMessage());
        }
        
        if(consistent) {
            return SignatureStatus.valid(chain);
        } else {
            return SignatureStatus.invalid("Incosistent");
        }
    }
}
